/** * WorthDoing.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: src/app/investigate/[id]/InvestigationLive.tsx * Description: Live investigation client view — SSE timeline, animating hypotheses, opportunities, streamed reports; mobile tabs. */ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { useInvestigationEvents, useSnapshot, type UiAgentEvent, } from "@/lib/ui/api"; import { Timeline } from "@/components/wd/Timeline"; import { HypothesisPanel } from "@/components/wd/HypothesisPanel"; import { StatsBar } from "@/components/wd/StatsBar"; import { PhaseBadge, InvestigationStatusBadge } from "@/components/wd/badges"; import { WorthScore } from "@/components/wd/OpportunityCard"; import { Markdown } from "@/components/wd/Markdown"; const STRUCTURAL_EVENTS = new Set([ "investigation.started", "phase.changed", "budget.updated", "hypothesis.created", "hypothesis.updated", "hypothesis.rejected", "evidence.saved", "opportunity.created", "report.completed", "investigation.completed", "investigation.failed", ]); type MobileTab = "timeline" | "hypotheses" | "opportunities"; export function InvestigationLive({ id }: { id: string }) { const { snapshot, refresh, error } = useSnapshot(id); const [events, setEvents] = useState([]); const [reportDraft, setReportDraft] = useState<{ opportunityId: string; text: string } | null>(null); const [tab, setTab] = useState("timeline"); const feedRef = useRef(null); const refreshTimer = useRef | null>(null); const onEvent = useCallback( (e: UiAgentEvent) => { if (e.type === "report.delta") { const oid = String(e.payload.opportunityId); setReportDraft((prev) => prev && prev.opportunityId === oid ? { opportunityId: oid, text: prev.text + String(e.payload.delta) } : { opportunityId: oid, text: String(e.payload.delta) }, ); return; } if (e.type === "report.started") setReportDraft({ opportunityId: String(e.payload.opportunityId), text: "" }); if (e.type === "report.completed") setReportDraft(null); setEvents((prev) => { if (prev.some((p) => p.seq === e.seq)) return prev; const next = [...prev, e].sort((a, b) => a.seq - b.seq); return next.length > 400 ? next.slice(next.length - 400) : next; }); if (STRUCTURAL_EVENTS.has(e.type)) { // Debounce snapshot refreshes so bursts don't stampede the API. if (refreshTimer.current) clearTimeout(refreshTimer.current); refreshTimer.current = setTimeout(refresh, 400); } }, [refresh], ); const { connected } = useInvestigationEvents(id, onEvent); // Keep the feed pinned to the latest event. useEffect(() => { const el = feedRef.current; if (el) el.scrollTop = el.scrollHeight; }, [events.length, reportDraft?.text.length]); if (error) { return (

Investigation not found

{error}

Start a new one
); } if (!snapshot) { return (
); } const inv = snapshot.investigation; const isLive = inv.status === "running" || inv.status === "pending"; const rightColumn = ( <>

Hypotheses {snapshot.hypotheses.length}

Opportunities {snapshot.opportunities.length}

{snapshot.opportunities.length === 0 ? (

none yet — opportunities appear when hypotheses survive the skeptic

) : (
    {snapshot.opportunities.map((o) => (
  • {o.title}

    {o.summary}

    {!o.hasReport && (

    report in progress…

    )}
  • ))}
)}
{snapshot.sources.length > 0 && (

Sources with evidence {snapshot.sources.length}

)} ); const timelinePane = (

Investigation log

{connected ? (isLive ? "live" : "replay") : "reconnecting"}
{reportDraft && (

writing report — streaming

{reportDraft.text}

)}
); return (
{inv.model}

{inv.objective}

{inv.status === "completed" && inv.conclusion && (

conclusion — {inv.outcome?.replaceAll("_", " ")}

{inv.conclusion}
)} {inv.status === "failed" && (

investigation failed

{inv.error ?? "Unknown error."}

)}
{/* Mobile tabs */}
{( [ ["timeline", "Timeline"], ["hypotheses", "Hypotheses"], ["opportunities", "Results"], ] as [MobileTab, string][] ).map(([key, label]) => ( ))}
{/* Mobile stacked view */}
{tab === "timeline" && timelinePane} {tab !== "timeline" && (
{tab === "hypotheses" ? (
) : ( rightColumn )}
)}
{/* Desktop split view */}
{timelinePane}
{rightColumn}
); }